Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds PostgreSQL master-replica read/write routing functionality to support database scalability. The implementation introduces a custom routing session that directs write operations to the master database while allowing read operations to use a replica database.
Changes:
- New database routing infrastructure with
RoutingSessionclass that routes database operations between master and replica - Configuration options for single or master-replica database modes
- Master database availability checks added to all write API endpoints
- Error handling for database unavailability with OperationalError suppression and user-facing error responses
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| app/database.py | New file implementing RoutingSession for master-replica routing and engine configuration |
| app/config.py | Added replica database configuration fields and removed engine property from Settings |
| app/ioc.py | Modified session factory to support routing mode, removed engine provider |
| app/api/utils.py | New utility function to check master database availability before write operations |
| app/multidirectory.py | Updated to use engines dictionary instead of settings.engine |
| tests/conftest.py | Updated test configuration to use engines dictionary |
| app/ldap_protocol/ldap_requests/base.py | Added RESPONSE_TYPE class variable and OperationalError handling in request handlers |
| app/ldap_protocol/ldap_requests/*.py | Added RESPONSE_TYPE class variable to request classes |
| app/ldap_protocol/ldap_requests/bind.py | Added OperationalError suppression for user login attribute updates |
| app/ldap_protocol/session_storage/repository.py | Added OperationalError suppression for session key creation |
| app/api/**/router*.py | Added check_master_db dependency to write endpoints |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
0cbb784 to
7891162
Compare
…nations and MFA callback
…pdate session handling
…ndencies across routers
| @provide(scope=Scope.APP) | ||
| def get_engine(self, settings: Settings) -> AsyncEngine: | ||
| """Get async engine.""" | ||
| return settings.engine | ||
| def get_engine_registry(self, settings: Settings) -> EngineRegistry: | ||
| return EngineRegistry( | ||
| master_engine=settings.engine, | ||
| replica_engine=settings.replica_engine, | ||
| ) | ||
|
|
||
| @provide(scope=Scope.APP) | ||
| def get_session_factory( | ||
| self, | ||
| engine: AsyncEngine, | ||
| settings: Settings, | ||
| engine_registry: EngineRegistry, | ||
| ) -> async_sessionmaker[AsyncSession]: | ||
| """Create session factory.""" | ||
| return async_sessionmaker(engine, expire_on_commit=False) | ||
| if settings.POSTGRES_RW_MODE == "single": | ||
| return async_sessionmaker( | ||
| bind=engine_registry.get_master_engine(), | ||
| expire_on_commit=False, | ||
| ) | ||
|
|
||
| return async_sessionmaker( | ||
| sync_session_class=RoutingSession, | ||
| expire_on_commit=False, | ||
| info={"engine_registry": engine_registry}, | ||
| ) |
There was a problem hiding this comment.
у нас теперь будет 2 кейса работы с бд:
- single: читаем из мастера, пишем в мастер
- replication: читаем из реплики, пишем в мастер (иногда и читаем из мастера)
но RoutingSession используется только тогда, когда у нас режим replication
я бы предложил всю работу с engine вести через прослойку RoutingSession для "шаблонности". т.е. даже в случае с single режимом мы работаем с RoutingSession, можно попробовать даже так: и для чтения и для записи использовать один и тот же engine
| if self.POSTGRES_RW_MODE != "replication": | ||
| return None |
There was a problem hiding this comment.
мб тут стоить райзить ошибку?
мы дергаем replica_engine значит это не просто так, значит значение переменной POSTGRES_RW_MODE обязано быть replication иначе дичь какая-то
| TCP_PACKET_SIZE: int = 1024 | ||
| COROUTINES_NUM_PER_CLIENT: int = 3 | ||
|
|
||
| POSTGRES_RW_MODE: Literal["single", "replication"] = "single" |
There was a problem hiding this comment.
[опционально] это я бы вынес в отдельный тип условно POSTGRES_RW_MODE_TYPE, хотя выглядит как излишнее, но мне просто не нравятся "магические переменные" в коде, хочется их попрятать
| @audit_router.put( | ||
| "/policy/{policy_id}", | ||
| error_map=error_map, | ||
| dependencies=[Depends(require_master_db)], |
There was a problem hiding this comment.
require_master_db в описании указано что это используется везде где идет запись.. боюсь что мы разработчики можем забывать указывать эту зависимость в будущих роутах, но я не могу придумать как перестраховаться.
There was a problem hiding this comment.
может имеет смысл сделать "базовый" роут в котором эта штука есть по умолчанию, внутри этой функции ведь есть защита от "избыточного" препинга когда у нас сингл
There was a problem hiding this comment.
а нет, смысла нет, т.к. при реализации мастер мастер этот препинг уйдет
Задача 1161
Изменения:
Settingsдобавлены:Engineпод реплику.single\replication.EngineRegistry, который включает в себяengineмастера и реплики.RoutingSession, отнаследованный отSession. В классе переопределены два метода:get_bind- метод, к которому обращается сессия перед выполнением запроса в базу (в момент вызовов.flush(),.execute(),.scalarsи т.д.) для получения бинда -EngineилиConnection.flush- в метод добавлено выставление дополнительного флага_force_masterдля чтения после записи.RESPONSE_TYPE(новый ClassVar для реквестов) нашего запроса со статус кодомLDAPCodes.UNAVAILABLE.Новый flow:
single- одна база и для чтения и для записи, то ничего не меняется, RoutingSession не используется.replication- то во время каждого запроса в базу мы в методеget_bindопределяем в каком мы сейчас состоянии сессии:_force_master, отдаем движок мастера._force_masterуже выставлен, значит отдаем движок мастера. (Например, случай Modify запроса, мы сначала выполняем запросы поиска в реплику, после этого выполняем flush для новой директории и в дальнейшем чтение в рамках запроса идет только из мастера)Открытые вопросы
sessionвioc? - там происходитsession.commit(). Если нам прилетитModifyзапрос во время отвала мастера мы упадем в.handleметоде с эксепшеномOperationalError, оно обработается выше в методе_handle_tcp\_handle_apiвbase.pyи LDAP нормально вернет ответ клиенту со статус кодомLDAPCodes.UNAVAILABLE, но при этом выйдет исключение вioc.pyуже после возвращения ответа клиенту. В целом не влияет на работу, но нужно как-то захендлить.